using System;
using System.Reflection;
using NHibernate;
using NHibernate.Properties;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper.NHibernate.Projections
{
    [Serializable]
    public class ComplexPropertyAccessor : IPropertyAccessor
    {
        public IGetter GetGetter(Type theClass, string propertyName)
        {
            throw new NotImplementedException();
        }

        public ISetter GetSetter(Type type, string propertyName)
        {
            var result = GetSetterOrNull(type, propertyName);
            if (result == null)
            {
                throw new PropertyNotFoundException(type, propertyName, "setter");
            }
            return result;
        }

        public bool CanAccessThroughReflectionOptimizer
        {
            get { throw new NotImplementedException(); }
        }

        private static ComplexSetter GetSetterOrNull(Type type, string propertyName)
        {
            if (type == typeof(object) || type == null)
            {
                // the full inheritance chain has been walked and we could
                // not find the Property get
                return null;
            }

            if (propertyName.Contains("."))
            {
                var parts = propertyName.Split('.');

                if (parts.Length == 0 || parts[0] == string.Empty) return null;

                var parentSetter = GetSetterOrNull(type, parts[0]);

                ComplexSetter innerSetter = null;
                for (var i = 1; i < parts.Length; i++)
                {
                    var innerType = innerSetter == null ? parentSetter.Property.PropertyType : innerSetter.Property.PropertyType;
                    innerSetter = GetSetterOrNull(innerType, parts[i]);
                    SetInnerSetter(parentSetter, innerSetter);
                }

                return parentSetter;
            }

            var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;

            if (type.IsValueType)
            {
                // the BindingFlags.IgnoreCase is important here because if type is a struct, the GetProperty method does
                // not ignore case by default. If type is a class, it _does_ ignore case... we're better off explicitly
                // stating that casing should be ignored so we get the same behavior for both structs and classes
                bindingFlags = bindingFlags | BindingFlags.IgnoreCase;
            }

            var property = type.GetProperty(propertyName, bindingFlags);

            if (property != null && property.CanWrite)
            {
                return new ComplexSetter(type, property, propertyName);
            }

            // recursively call this method for the base Type
            var setter = GetSetterOrNull(type.BaseType, propertyName);

            // didn't find anything in the base class - check to see if there is 
            // an explicit interface implementation.
            if (setter == null)
            {
                var interfaces = type.GetInterfaces();
                for (var i = 0; setter == null && i < interfaces.Length; i++)
                {
                    setter = GetSetterOrNull(interfaces[i], propertyName);
                }
            }

            return setter;
        }

        private static void SetInnerSetter(ComplexSetter parentSetter, ComplexSetter innerSetter)
        {
            if (parentSetter.InnerSetter == null)
            {
                parentSetter.InnerSetter = innerSetter;
            }
            else
            {
                SetInnerSetter(parentSetter.InnerSetter, innerSetter);
            }
        }
        
        [Serializable]
        public sealed class ComplexSetter : ISetter
        {
            private readonly Type _clazz;
            private readonly PropertyInfo _property;
            private readonly string _propertyName;
            private ComplexSetter _innerSetter;

            public ComplexSetter(Type clazz, PropertyInfo property, string propertyName, ComplexSetter innerSetter = null)
            {
                _clazz = clazz;
                _property = property;
                _propertyName = propertyName;
                _innerSetter = innerSetter;
            }

            public PropertyInfo Property
            {
                get { return _property; }
            }
            
            public void Set(object target, object value)
            {
                try
                {
                    if (_innerSetter != null)
                    {
                        var innerValue = _property.GetValue(target, new object[0]) ?? ObjectFactory.Create(_innerSetter._clazz);

                        _innerSetter.Set(innerValue, value);

                        value = innerValue;
                    }

                    _property.SetValue(target, value, new object[0]);
                }
                catch (ArgumentException ae)
                {
                    // if I'm reading the msdn docs correctly this is the only reason the ArgumentException
                    // would be thrown, but it doesn't hurt to make sure.
                    if (_property.PropertyType.IsInstanceOfType(value) == false)
                    {
                        // add some details to the error message - there have been a few forum posts an they are 
                        // all related to an ISet and IDictionary mixups.
                        var msg = String.Format("The type {0} can not be assigned to a property of type {1}", value.GetType(), _property.PropertyType);
                        throw new PropertyAccessException(ae, msg, true, _clazz, _propertyName);
                    }

                    throw new PropertyAccessException(ae, "ArgumentException while setting the property value by reflection", true, _clazz, _propertyName);
                }
                catch (Exception e)
                {
                    throw new PropertyAccessException(e, "could not set a property value by reflection", true, _clazz, _propertyName);
                }
            }

            public string PropertyName
            {
                get { return _property.Name; }
            }

            public MethodInfo Method
            {
                get { throw new NotImplementedException(); }
            }

            public ComplexSetter InnerSetter
            {
                get { return _innerSetter; }
                set { _innerSetter = value; }
            }
        }
    }
}
